package cn.seecoder.ai.enums;

import cn.seecoder.ai.exception.AIExternalException;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Getter;

import java.util.Map;

import static cn.seecoder.ai.utils.DataTypeHelper.*;

/**
 * @author fanyanpeng
 * @date 2023/3/22 19:32
 */
@Getter
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum TrainParamEnum {

    FEATURE_DIMENSION_NUM(
            "FEATURE_DIMENSION_NUM",
            "featureDimensionNum",
            "特征数目",
            ParamTypeEnum.INT,
            "特征维度，没有默认值",
            "特征向量的维度",
            -1,
            1,
            1<<20
    ),
    LABEL_UNIQUE_NUM(
            "LABEL_UNIQUE_NUM",
            "labelUniqueNum",
            "标签的类型数目",
            ParamTypeEnum.INT,
            "标签数目，没有默认值",
            "标签数目，没有默认值",
            -1,
            1,
            1<<20
    ),

    MAX_CATEGORIES(
            "MAX_CATEGORIES",
            "maxCategories",
            "最大种类",
            ParamTypeEnum.INT,
            "参数用于指定类别特征（即离散型特征）最多的数量。如果一个特征的不同取值超过了MAX_CATEGORIES，那么VectorIndexer会把这个特征视为连续型特征（即数值型特征），并且对这个特征不会进行类别特征的处理。这个参数的值越大，VectorIndexer能够处理的类别特征的数量就越多，但是会增加计算开销。",
            "特征被认为是离散特征的上限",
            20,
            1,
            1<<20
    ),
    TRAIN_DATA_SET_OCCUPY(
            "TRAIN_DATA_SET_OCCUPY",
            "trainingDataSetOccupy",
            "训练集占比",
            ParamTypeEnum.DOUBLE,
            "选定为训练集的比例，取值应该在(0.0,1.0)",
            "训练集的比例",
            0.7,
            0.001,
            0.999),

    //LogisticRegression, from https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/ml/LogisticRegressionExample.scala
    REG_PARAM(
            "REG_PARAM",
            "regParam",
            "正则参数",
            ParamTypeEnum.DOUBLE,
            "regularization parameter.正则化参数。正则化通过对模型参数的值进行惩罚来防止过拟合。正则化参数的值越大，模型就越不容易过拟合。",
            "正则化通过对模型参数的值进行惩罚来防止过拟合",
            0.0,
            0.0,
            1.0*(1<<20)
    ),
    MAX_ITER(
            "MAX_ITER",
            "maxIter",
            "最大迭代次数",
            ParamTypeEnum.INT,
            "maximum number of iterations",
            "最大迭代次数", 100,
            1,
            1<<20
    ),
    ELASTIC_NET_PARAM(
            "ELASTIC_NET_PARAM",
            "elasticNetParam",
            "elasticNetParam",
            ParamTypeEnum.DOUBLE,
            "用于控制Elastic-Net混合比例的参数，其中Elastic-Net是一种用于线性回归和逻辑回归等的正则化方法。当值为0时，它等同于L1正则化，当值为1时，它等同于L2正则化。L1正则化更倾向于使一些权重为0，从而达到特征选择的目的，可以用于降维或特征选择。L2正则化更倾向于使每个权重都比较小，从而防止过拟合。",
            "用于控制Elastic-Net混合比例的参数", 0.0,
            0.0,
            1.0
    ),


    //DecisionTree,from https://github.com/apache/spark/blob/master/examples/src/main/scala/org/apache/spark/examples/ml/DecisionTreeExample.scala
    MAX_DEPTH(
            "MAX_DEPTH",
            "maxDepth",
            "最大深度",
            ParamTypeEnum.INT,
            "Maximum depth of the tree. (Nonnegative)" +
                    " E.g., depth 0 means 1 leaf node; depth 1 means 1 internal node + 2 leaf nodes." +
                    " Must be in range [0, 30].",
            "数的最大深度",
            5,
            0,
            30
    ),
    MAX_BINS(
            "MAX_BINS",
            "maxBins",
            "最大分箱",
            ParamTypeEnum.INT,
            "Max number of bins for" +
                    " discretizing continuous features.  Must be at least 2 and at least number of categories" +
                    " for any categorical feature.",
            "非离散数据分箱的最大数目",
            32,
            2,
            1<<20
    ),
    MIN_INSTANCES_PER_NODE(
            "MIN_INSTANCES_PER_NODE",
            "minInstancesPerNode",
            "每节点最小实例",
            ParamTypeEnum.INT,
            "Minimum" +
                    " number of instances each child must have after split.  If a split causes the left or right" +
                    " child to have fewer than minInstancesPerNode, the split will be discarded as invalid." +
                    " Must be at least 1.",
            "子节点切分后所含有的最小实例数",
            1,
            1,
            1<<20
    ),
    MIN_INFO_GAIN(
            "MIN_INFO_GAIN",
            "minInfoGain",
            "最小信息增益",
            ParamTypeEnum.DOUBLE,
            "Minimum information gain for a split to be considered at a tree node.",
            "切分树节点的最小信息增益",
            0.0,
            0.0,
            1.0*(1<<20)
    ),
    K(
            "K",
            "k",
            "K值",
            ParamTypeEnum.INT,
            "聚类算法中的目标种类数目，必须大于1",
            "聚类算法中的目标种类数目",
            2,
            2,
            1<<20
    ),
    SEED(
            "SEED",
            "seed",
            "随机数种子",
            ParamTypeEnum.LONG,
            "随机数种子，数值大于1",
            "descriptionZH",
            2023L,
            2L,
            1L<<20
    ),
    NUM_TREES(
            "NUM_TREES",
            "numTrees",
            "决策树的数量",
            ParamTypeEnum.INT,
            "决策树的数量，通常，增加NUM_TREES可以提高模型的准确度，但是当NUM_TREES达到一定值时，模型的性能将不再提高，而且增加的NUM_TREES将浪费计算资源。",
            "决策树的数量",
            2,
            2,
            1<<20),

    BLOCK_SIZE(
            "BLOCK_SIZE",
            "blockSize",
            "块大小",
            ParamTypeEnum.INT,
            "block size for stacking input data in matrices. Data is stacked within partitions. If block size is more than remaining data in a partition then it is adjusted to the size of this data.",
            "输入块的大小", 128,
            2,
            1<<20
    )

    ;

    private final String self;
    private final String paramName;

    private final String paramNameZH;
    private final ParamTypeEnum paramType;
    private final String description;

    private final String descriptionZH;
    private final Object defaultValue;

    private final Object leftBound;

    private final Object rightBound;


    /**
     * 验证value在当前参数范围之内
     * @author   fanyanpeng
     * @date 2023/4/20 1:26
     * @param value
     * @return java.lang.Boolean
     */
    public Boolean validInRange(Object value){
        boolean outOfRange=false;
        try{
            switch (paramType){
                case INT:{
                    outOfRange = assertOutOfRange((Integer)leftBound,(Integer)rightBound,toInteger(value.toString()));
                    break;
                }
                case LONG:{
                    outOfRange = assertOutOfRange((Long) leftBound,(Long) rightBound,toLong(value));
                    break;
                }
                case DOUBLE:{
                    outOfRange = assertOutOfRange((Double)leftBound,(Double)rightBound,toDouble(value.toString()));
                    break;
                }
            }
            //处理转换出现异常的问题
        }catch (ClassCastException classCastException){
            classCastException.printStackTrace();
            throw AIExternalException.trainParamCastFailed(value,paramType.toString());
        }
        catch (NumberFormatException numberFormatException){
            numberFormatException.printStackTrace();
            throw AIExternalException.trainPrammNumberFormatException(value,paramType.toString());
        }
        //若超出范围则报错
        if(outOfRange){
            throw AIExternalException.trainParamOutOfRange(leftBound,rightBound,value,paramName);
        }
        return true;
    }


    // 使用范型，判断value是否在两个数值之间
    static private <T extends Comparable> Boolean assertOutOfRange(T left, T right, T value){
        if(value.compareTo(left)<0 || value.compareTo(right)>0){
            return true;
        }
        return false;
    }


    public Object readTrainParamMap(Map<TrainParamEnum,Object> params){
        return params.getOrDefault(this,this.defaultValue);
    }

    TrainParamEnum(String self, String paramName, String paramNameZH, ParamTypeEnum paramType, String description, String descriptionZH, Object defaultValue, Object leftBound, Object rightBound) {
        this.self = self;
        this.paramName = paramName;
        this.paramNameZH = paramNameZH;
        this.paramType = paramType;
        this.description = description;
        this.descriptionZH = descriptionZH;
        this.defaultValue = defaultValue;
        this.leftBound = leftBound;
        this.rightBound = rightBound;
    }


    public String getSelf() {
        return self;
    }

    public String getParamName() {
        return paramName;
    }

    public ParamTypeEnum getParamType() {
        return paramType;
    }

    public String getDescription() {
        return description;
    }

    public Object getDefaultValue() {
        return defaultValue;
    }

    public Object getLeftBound() {
        return leftBound;
    }

    public Object getRightBound() {
        return rightBound;
    }
}


